package com.frinder.official.akka.fsm;

import akka.actor.AbstractFSM;
import akka.actor.ActorRef;
import akka.japi.pf.UnitMatch;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import scala.concurrent.duration.Duration;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import static com.frinder.official.akka.fsm.Buncher.State.Active;
import static com.frinder.official.akka.fsm.Buncher.State.Idle;
import static com.frinder.official.akka.fsm.Buncher.Uninitialized.Uninitialized;

/**
 * Created by frinder on 2017/10/5.
 */
@Component
@Scope("prototype")
public class Buncher extends AbstractFSM<Buncher.State, Buncher.Data> {

    @Override
    public void preStart() throws Exception {
        super.preStart();

        startWith(Idle, Uninitialized);

        when(
                Idle,
                matchEvent(
                        SetTarget.class,
                        Uninitialized.class,
                        (setTarget, uninitialized) -> {
                            log().info("when Idle: {}, {}", setTarget, uninitialized);
                            return stay().using(new Todo(setTarget.getRef(), new LinkedList<>()));
                        }
                )
        );

        onTransition(
                matchState(Active, Idle,
                        () -> {
                            // reuse this matcher
                            final UnitMatch<Data> m = UnitMatch.create(
                                    matchData(Todo.class,
                                            todo -> {
                                                log().info("onTransition: {} to {}, todo: {}-{}", Active, Idle, todo, todo.hashCode());
                                                todo.getTarget().tell(new Batch(todo.getQueue()), getSelf());
                                            }
                                    )
                            );
                            m.match(stateData());
                        }
                ).state(Idle, Active,
                        () -> {
                            /* Do something here */
                            log().info("onTransition: {} to {}", Idle, Active);
                        }
                )
        );

        when(
                Active,
                Duration.create(1, "second"),
                matchEvent(
                        Arrays.asList(Flush.class, StateTimeout()),
                        Todo.class,
                        (event, todo) -> {
                            Todo copyTodo = todo.copy(new LinkedList<>());
                            log().info("when Active: {}, todo:{}-{}", event, todo, todo.hashCode());
                            log().info("when Active: {}, copyTodo:{}-{}", event, copyTodo, copyTodo.hashCode());
                            return goTo(Idle).using(copyTodo);
                        }
                )
        );

        whenUnhandled(
                matchEvent(
                        Queue.class,
                        Todo.class,
                        (queue, todo) -> {
                            log().info("whenUnhandled: {}, {}-{}", queue, todo, todo.hashCode());
                            return goTo(Active).using(todo.addElement(queue.getObj()));
                        }
                ).anyEvent((event, state) -> {
                    log().warning("received unhandled request {} in state {}/{}",
                            event, stateName(), state);
                    return stay();
                })
        );

        initialize();
    }


    public static final class Batch {

        private final List<Object> list;

        public Batch(List<Object> list) {
            this.list = list;
        }

        public List<Object> getList() {
            return list;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Batch batch = (Batch) o;

            return list.equals(batch.list);
        }

        @Override
        public int hashCode() {
            return list.hashCode();
        }

        @Override
        public String toString() {
            final StringBuilder builder = new StringBuilder();
            builder.append("Batch{list=");
            list.stream().forEachOrdered(e -> {
                builder.append(e);
                builder.append(",");
            });
            int len = builder.length();
            builder.replace(len, len, "}");
            return builder.toString();
        }

    }


    public static final class Queue {

        private final Object obj;

        public Queue(Object obj) {
            this.obj = obj;
        }

        public Object getObj() {
            return obj;
        }

        @Override
        public String toString() {
            return "Queue{" +
                    "obj=" + obj +
                    '}';
        }

    }


    public enum Flush {
        Flush
    }

    public static final class SetTarget {

        private final ActorRef ref;

        public SetTarget(ActorRef ref) {
            this.ref = ref;
        }

        public ActorRef getRef() {
            return ref;
        }

        @Override
        public String toString() {
            return "SetTarget{" +
                    "ref=" + ref +
                    '}';
        }
    }


    // states
    enum State {
        Idle, Active
    }

    // state data
    interface Data {
    }

    enum Uninitialized implements Data {
        Uninitialized
    }

    final class Todo implements Data {
        private final ActorRef target;
        private final List<Object> queue;

        public Todo(ActorRef target, List<Object> queue) {
            this.target = target;
            this.queue = queue;
        }

        public ActorRef getTarget() {
            return target;
        }

        public List<Object> getQueue() {
            return queue;
        }

        @Override
        public String toString() {
            return "Todo{" +
                    "target=" + target +
                    ", queue=" + queue +
                    '}';
        }

        public Todo addElement(Object element) {
            List<Object> nQueue = new LinkedList<>(queue);
            nQueue.add(element);
            return new Todo(this.target, nQueue);
        }

        public Todo copy(List<Object> queue) {
            return new Todo(this.target, queue);
        }

        public Todo copy(ActorRef target) {
            return new Todo(target, this.queue);
        }
    }

}
